# -*- coding: utf-8 -*-
# 本程序实现阿里云 DDNS
# - 阿里云SDK文档：https://api.aliyun.com/api-tools/sdk/Alidns?version=2015-01-09&language=python-tea&tab=primer-doc
# - DDNS 示例： https://api.aliyun.com/api-tools/demo/Alidns/7a52d9f9-59b0-43bd-96fc-8d475220f56f
# TODO: 
# - 实现 gui，方便配置、查看日志、查看状态。考虑用 ttkbootstrap 框架
# - 实现配置文件读写，将运行间隔、日志级别等写入
# - 利用 aiohttp模块实现 web 界面，方便查看日志、查看状态


# 标准库
import ast
import asyncio
import configparser
from datetime import datetime
import os
import re
from pathlib import Path
import subprocess
import logging
from ipaddress import IPv6Address, AddressValueError
from aiohttp import web
import json

# 第三方库
from alibabacloud_alidns20150109 import client as ali_domain_client
from alibabacloud_tea_openapi import models as ali_domain_models
from alibabacloud_alidns20150109.models import UpdateDomainRecordRequest, DescribeDomainRecordsRequest
from alibabacloud_tea_util.models import RuntimeOptions
import telnetlib3
from wxpusher import WxPusher


# 配置常量
CONFIG_FILE = 'ddns.ini'
DEFAULT_CONFIG = {
    'General': {
        'log_level': 'INFO',
        'log_file': 'ddns.log',
        'interface_name': '以太网 2',
        'update_dns_interval': '60'
    },
    'IP': {
        'current_ip': '',
        'telnet_port': '8888',
        'web_port': '8889'
    }
}

def initialize_config():
    config = configparser.ConfigParser()
    if not Path(CONFIG_FILE).exists():
        config.read_dict(DEFAULT_CONFIG)
        with open(CONFIG_FILE, 'w', encoding='utf-8') as configfile:
            config.write(configfile)
    else:
        config.read(CONFIG_FILE, encoding='utf-8')
        # 确保所有默认配置项都存在
        for section, options in DEFAULT_CONFIG.items():
            if not config.has_section(section):
                config.add_section(section)
            for option, value in options.items():
                if not config.has_option(section, option):
                    config.set(section, option, value)
        # 保存更新后的配置
        with open(CONFIG_FILE, 'w', encoding='utf-8') as configfile:
            config.write(configfile)        
    return config

config = initialize_config()

logging.basicConfig(
    filename=config['General']['log_file'],
    level=config['General']['log_level'] ,
    format='%(asctime)s - %(levelname)s - %(message)s',
    encoding='utf-8',
)
logging.warning("DDNS 程序启动。")
print("DDNS 程序启动。")

# 配置信息
required_env_vars = [
    'ALIBABA_CLOUD_DOMAIN_NAME', 
    'ALIBABA_CLOUD_DOMAIN_NAME_RR',
    'ALIBABA_CLOUD_DOMAIN_NAME_RR_IPV6_SUFFIX', 
    'ALIBABA_CLOUD_ACCESS_KEY_ID',
    'ALIBABA_CLOUD_ACCESS_KEY_SECRET', 
    'WXPUSHER_APP_TOKEN', 
    'WXPUSHER_UIDS'
]
for var in required_env_vars:
    if var not in os.environ:
        logging.error(f"环境变量 {var} 未设置，请检查并设置后重新运行程序。")
        print(f"环境变量 {var} 未设置，请检查并设置后重新运行程序。")
        exit(1)
DOMAIN_NAME = os.environ['ALIBABA_CLOUD_DOMAIN_NAME']  
RR = os.environ['ALIBABA_CLOUD_DOMAIN_NAME_RR']
IPV6_SUFFIX = os.environ['ALIBABA_CLOUD_DOMAIN_NAME_RR_IPV6_SUFFIX']
INTERFACE_NAME = config['General']['interface_name']
IPV6_LINE_PATTERN = re.compile(
        r'^\s*(IPv6 地址).*?:\s*(.*?)\s*$',
        re.IGNORECASE
    )

# 全局变量
ddns_server_running = True  # 用于控制 DDNS 运行状态
ddns_start_time = datetime.now()
web_server_running = True


def get_original_ip():
    return config['IP'].get('current_ip', '')

def save_current_ip(ip):
    config['IP']['current_ip'] = ip
    with open(CONFIG_FILE, 'w', encoding='utf-8') as configfile:
        config.write(configfile)


def extract_ipv6_addresse_from_ipconfig_output(text, adapter_name, suffix):
    # 标准化后缀：拆分并转换为四位小写格式
    suffix_parts = []
    for part in suffix.split(':'):
        normalized_part = part.zfill(4).lower()  # 补零到四位并转小写
        suffix_parts.append(normalized_part)
    if len(suffix_parts) != 4:
        raise ValueError("后缀必须包含4个IPv6段")
    
    # 正则匹配IPv6地址行
    current_adapter = False
    results = []
    
    for line in text.split('\n'):
        stripped_line = line.strip()
        
        # 检测目标网卡段落
        if stripped_line.startswith(f"以太网适配器 {adapter_name}:"):
            current_adapter = True
        elif stripped_line.startswith("以太网适配器"):
            current_adapter = False
        
        if current_adapter:
            # 匹配IPv6地址行
            match = IPV6_LINE_PATTERN.match(line)
            if match:
                address_str = match.group(2).split('%')[0]  # 去除%后缀
                try:
                    addr = IPv6Address(address_str)
                    exploded = addr.exploded.split(':')     # 展开为8段
                    last_four = [part.lower() for part in exploded[-4:]]  # 取最后四段
                    
                    if last_four == suffix_parts:
                        results.append(addr.compressed)      # 使用压缩格式存储
                except AddressValueError:
                    continue
    
    return results

def get_current_ipv6():
    result = subprocess.run(['ipconfig',], capture_output=True, text=True).stdout
    ipv6_addresses = extract_ipv6_addresse_from_ipconfig_output(result, INTERFACE_NAME, IPV6_SUFFIX)
    if ipv6_addresses:
        return ipv6_addresses[0]
    else:
        return None


# 配置阿里云域名管理
def create_ali_domain_client():
    config = ali_domain_models.Config(
        access_key_id=os.environ['ALIBABA_CLOUD_ACCESS_KEY_ID'],
        access_key_secret=os.environ['ALIBABA_CLOUD_ACCESS_KEY_SECRET'],
        endpoint='alidns.cn-hangzhou.aliyuncs.com',  # 固定值
        region_id='cn-hangzhou'  # 固定值
    )
    return ali_domain_client.Client(config)

def update_aaaa_record(client, record_id, rr, new_ipv6):
    request = UpdateDomainRecordRequest(
        record_id=record_id,
        rr=rr,
        type='AAAA',
        value=new_ipv6
    )
    runtime = RuntimeOptions()
    try:
        response = client.update_domain_record_with_options(request, runtime)
        return response.body
    except Exception as e:
        logging.error(f"更新记录时发生错误：{e}")
        return None



def query_aaaa_record(client, domain_name, rr):
    request = DescribeDomainRecordsRequest(
        domain_name=domain_name,
        rrkey_word=rr,
        type='AAAA'  # 查询 AAAA 记录
    )
    
    runtime = RuntimeOptions()

    try:
        response = client.describe_domain_records_with_options(request, runtime)
        records = response.body.domain_records
        if records :
            return records.record[0].record_id, records.record[0].value
    except Exception as e:
        logging.error(f"查询记录时发生错误：{e}")
        return None, None
    

def send_wx(msg):
    try:
        app_token = os.environ['WXPUSHER_APP_TOKEN']
        uids = os.environ['WXPUSHER_UIDS']
        uids = ast.literal_eval(uids)
        result = WxPusher.send_message(token=app_token, uids=uids, content=msg)
        return result
    except Exception as e:
        logging.error(f"发送微信消息时发生错误: {e}")
        return None


async def handle_telnet_client(reader, writer):
    global ddns_server_running

    help_msg = "Commands:\n\r" \
                "- 'status': Check current status\n\r" \
                "- 'exit': Exit telnet server\n\r" \
                "- 'warning': set log level to warning\n\r" \
                "- 'info': set log level to info\n\r" \
                "- 'log': show log\n\r" \
                "- 'quit': Exit the main DDNS loop\n\r" \
                "- 'help': Show this help message\n\r"
    welcome_msg = f"Welcome to DDNS Control Center!\n\r{help_msg}" 
    writer.write(welcome_msg)

    input_buffer = ''

    while True:
        data = await reader.read(1)
        if not data:
            break
        if data == '\b':  # 处理退格键
            if input_buffer:
                input_buffer = input_buffer[:-1]
                writer.write('\b \b')
        else:
            writer.write(data)
            input_buffer += data

        if data == '\n':
            command = input_buffer.strip().lower()
            input_buffer = ''

            if command == 'status':
                original_ip = get_original_ip()
                current_ip = get_current_ipv6()
                ddns_status = "Running" if ddns_server_running else "Stopped"
                logging_status = logging.getLevelName(logging.getLogger().getEffectiveLevel())
                elapsed_time = datetime.now() - ddns_start_time
                days = elapsed_time.days
                hours, remainder = divmod(elapsed_time.seconds, 3600)
                minutes, seconds = divmod(remainder, 60)
                elapsed_time_str = f"{days} days, {hours:02d}:{minutes:02d}:{seconds:02d}"
                msg = f"DDNS Status: {ddns_status}\n\r" \
                        f"Original IP: {original_ip}\n\r" \
                        f"Current IP: {current_ip}\n\r" \
                        f"logging level: {logging_status}\n\r" \
                        f"Elapsed Time: {elapsed_time_str}\n\r" \
                        f"Update Interval: {config['General']['update_dns_interval']} seconds\n\r"
            elif command == 'exit':
                writer.write("Exiting...\n\r")
                break
            elif command == 'warning':
                logging.getLogger().setLevel(logging.WARNING)
                config['General']['log_level'] = 'WARNING'
                msg = "Log level set to WARNING\n\r"
            elif command == 'info':
                logging.getLogger().setLevel(logging.INFO)
                config['General']['log_level'] = 'INFO'
                msg = "Log level set to INFO\n\r"
            elif command == 'quit':
                ddns_server_running = False
                msg = "Exiting DDNS loop.\n\rYou must type 'exit' to exit the telnet server.\n\r"
            elif command == 'help':
                msg = help_msg
            elif command == 'log':
                log_file = config['General']['log_file']
                try:
                    with open(log_file, 'r', encoding='utf-8') as log_file:
                        lines = log_file.readlines()
                        log_content = '\r'.join(lines[-10:])  # 显示最后10行日志
                        msg = f'{log_content} \n\r'
                except FileNotFoundError:
                    msg = "日志文件未找到 \n\r"
            else:
                msg = f"Unknown command '{command}', use 'help' for help!\n\r"
            writer.write(msg)

    writer.close()
                      
async def start_telnet_server():
    telnet_port = config['IP']['telnet_port']
    server = await telnetlib3.create_server(
       host='::',
       port=telnet_port,
       shell=handle_telnet_client, 
    )
    logging.warning(f"Telnet server started on port {telnet_port}.")
    await server.wait_closed()
    logging.warning("Telnet server closed.")

async def ddns_main_loop():
    global ddns_server_running
    while ddns_server_running:
        try:
            if get_original_ip() == '':
                save_current_ip(get_current_ipv6())
            original_ip = get_original_ip()
            current_ip = get_current_ipv6()

            if original_ip is None or current_ip is None:
                error_msg = "【DDNS】查询IP失败，详见系统运行日志。💔💔"
                logging.error(error_msg)
                send_wx(error_msg)
            elif current_ip != original_ip:
                client = create_ali_domain_client()
                record_id, old_ip = query_aaaa_record(client, DOMAIN_NAME, RR)

                if record_id is None:
                    error_msg = "【DDNS】查询记录失败，详见系统运行日志。💔💔"
                    logging.error(error_msg)
                    send_wx(error_msg)
                elif current_ip != old_ip:
                    print("IP 地址已发生变化，需要更新。")
                    save_current_ip(current_ip)
                    result = update_aaaa_record(client, record_id, RR, current_ip)
                    msg = f"【DDNS】域名 {RR}.{DOMAIN_NAME} 已更新：{old_ip} ==> {current_ip} ， 结果：{result}"
                    logging.warning(msg)
                    send_wx(msg)
                else:
                    #logging.info("IP 地址未发生变化，无需更新。")
                    pass
            else:
                #logging.info("IP 地址未发生变化，无需更新。")
                print("IP 地址未发生变化，无需更新。")

        except Exception as e:
            error_msg = f"程序发生未知错误: {e}"
            logging.error(error_msg)
            send_wx(f"【DDNS】程序发生未知错误，请检查日志。错误信息: {e}")

        # 休眠若干秒
        await asyncio.sleep(int(config['General']['update_dns_interval']))

async def web_handler(request):
    """处理Web请求"""
    original_ip = get_original_ip()
    current_ip = get_current_ipv6()
    ddns_status = "Running" if ddns_server_running else "Stopped"
    logging_status = logging.getLevelName(logging.getLogger().getEffectiveLevel())
    elapsed_time = datetime.now() - ddns_start_time
    
    status_data = {
        "status": ddns_status,
        "original_ip": original_ip,
        "current_ip": current_ip,
        "logging_level": logging_status,
        "elapsed_time": str(elapsed_time),
        "update_interval": config['General']['update_dns_interval']
    }
    
    return web.Response(
        text=json.dumps(status_data, ensure_ascii=False),
        content_type="application/json"
    )

async def start_web_server():
    """启动Web服务器"""
    app = web.Application()
    app.router.add_get('/', web_handler)
    
    runner = web.AppRunner(app)
    await runner.setup()
    port = config['IP']['web_port']
    site = web.TCPSite(runner, '::', port)
    
    logging.warning(f"Web server started on port {port}.")
    await site.start()
    
    while web_server_running:
        await asyncio.sleep(1)
    
    await runner.cleanup()
    logging.warning("Web server stopped.")

async def main():
    telnet_task = asyncio.create_task(start_telnet_server())
    web_task = asyncio.create_task(start_web_server())
    ddns_task = asyncio.create_task(ddns_main_loop())
    await asyncio.gather(telnet_task, web_task, ddns_task)


if __name__ == '__main__':
    asyncio.run(main())
    
